package org.apache.catalina.session;

import java.io.IOException;
import java.util.Set;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Session;
import org.apache.catalina.session.JedisUtil.Callback;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.Protocol;

/**
 * @see https://github.com/xetorthio/redis-store.git
 */
public class JedisStore extends StoreBase {
	static Log log = LogFactory.getLog(JedisStore.class);
	JedisPoolConfig poolConfig = new JedisPoolConfig();
	JedisPool pool = null;
	JedisCluster cluster = null;
	String host = Protocol.DEFAULT_HOST;
	int port = Protocol.DEFAULT_PORT;
	int timeout = Protocol.DEFAULT_TIMEOUT;
	String password = null;
	int database = Protocol.DEFAULT_DATABASE;
	int maxInactiveInterval = 1800;
	String prefix = "";
	String hostAndPorts = null;
	String sentinelMaster = null;
	JedisSentinelPool sentinel = null;

	public void clear() throws IOException {
		int size = JedisUtil.exec(pool, sentinel, cluster, new Callback<Integer>() {
			public Integer execute(Jedis jedis) {
				if(prefix.length()==0) {
					int dbSize = jedis.dbSize().intValue();
					jedis.flushDB();
					return dbSize;
				}else {
					Set<String> keySet = jedis.keys(prefix+"*");
					for(String key : keySet) {
						jedis.del(key);
					}
					return keySet.size();
				}
			}
		});
		log.debug(String.valueOf(size));
	}

	public int getSize() throws IOException {
		int size = JedisUtil.exec(pool, sentinel, cluster, new Callback<Integer>() {
			public Integer execute(Jedis jedis) {
				if(prefix.length()==0) {
					return jedis.dbSize().intValue();
				}else {
					Set<String> keySet = jedis.keys(prefix+"*");
					return keySet.size();
				}
			}
		});
		log.debug(String.valueOf(size));
		return size;
	}

	public String[] keys() throws IOException {
		String[] keys = JedisUtil.exec(pool, sentinel, cluster, new Callback<String[]>() {
			public String[] execute(Jedis jedis) {
				if(prefix.length()==0) {
					Set<String> keySet = jedis.keys("*");
					return keySet.toArray(new String[keySet.size()]);
				}else {
					Set<String> keySet = jedis.keys(prefix+"*");
					String[] keys = keySet.toArray(new String[keySet.size()]);
					int len = keys.length, sub = prefix.length();
					for(int i=0;i<len;i++) {
						keys[i] = keys[i].substring(sub);
					}
					return keys;
				}
			}
		});
		log.debug(keys.length);
		return keys;
	}

	public Session load(final String id) throws ClassNotFoundException, IOException {
		final String sid = prefix + id;
		StandardSession session = JedisUtil.exec(pool, sentinel, cluster, new Callback<StandardSession>() {
			public StandardSession execute(Jedis jedis) {
				byte[] key = sid.getBytes();
				byte[] bs = jedis.get(key);
				if(bs == null) {
					return null;
				}
				StandardSession session = (StandardSession) manager.createEmptySession();
				session = JedisUtil.deserialize(session, bs);
				if(session == null) {
					jedis.del(key);
				}
				return session;
			}
		});
		if(session != null) {
			log.debug(sid);
			session.setManager(manager);
			return session;
		}
		return null;
	}

	public void remove(String id) throws IOException {
		String sid = prefix+id;
		Boolean remove = JedisUtil.exec(pool, sentinel, cluster, new Callback.DELETE(sid));
		log.debug(sid+" remove="+remove);
	}

	public void save(final Session session) throws IOException {
		final String sid = prefix+session.getId();
		Boolean save = JedisUtil.exec(pool, sentinel, cluster, new Callback<Boolean>() {
			public Boolean execute(Jedis jedis) {
				byte[] bs = JedisUtil.serialize((StandardSession)session);
				return "OK".equals(jedis.setex(sid.getBytes(), maxInactiveInterval, bs));
			}
		});
		log.debug(sid+" save="+save);
	}

	protected synchronized void startInternal() throws LifecycleException {
		setState(LifecycleState.STARTING);
		JedisUtil.setClassLoader(getManager());
		log.info("JedisPool maxTotal="+poolConfig.getMaxTotal()+", maxIdle="+poolConfig.getMaxIdle()+", minIdle="+poolConfig.getMinIdle()+", lifo="+poolConfig.getLifo());
		Set<HostAndPort> nodes = JedisUtil.nodes(hostAndPorts);
		if(nodes!=null && nodes.size()>1) {
			if(JedisUtil.isBlank(sentinelMaster)) {
				cluster = new JedisCluster(nodes, poolConfig);
				log.info("JedisCluster nodes="+nodes);
			}else {
				Set<String> sentinels = JedisUtil.sentinels(nodes);
				log.info("JedisSentinel master="+sentinelMaster+",timeout="+timeout+",database="+database+" "+sentinels.size()+" sentinels="+sentinels);
				sentinel = new JedisSentinelPool(sentinelMaster, sentinels, poolConfig, timeout, password, database);
			}
		}else {
			pool = new JedisPool(poolConfig, host, port, timeout, password, database);
			log.info("JedisStore host="+host+", port="+port+", database="+database+", timeout="+timeout+", prefix="+prefix+", maxInactiveInterval="+maxInactiveInterval);
		}
	}

	protected synchronized void stopInternal() throws LifecycleException {
		setState(LifecycleState.STOPPING);
		JedisUtil.close(pool, sentinel, cluster);
	}

	public String getStoreName() {
		return "JedisStore";
	}

	public void setHost(String host) { this.host = host; }
	public void setPort(int port) { this.port = port; }
	public void setTimeout(int timeout) { this.timeout = timeout; }
	public void setPassword(String password) { this.password = password == null || (password=password.trim()).length()==0 ? null : password; }
	public void setDatabase(int database) { this.database = database; }
	public void setPrefix(String prefix) { this.prefix = JedisUtil.firstNonBlank(prefix); }
	public void setSentinelMaster(String sentinelMaster) { this.sentinelMaster = sentinelMaster; }
	public void setMaxInactiveInterval(int maxInactiveInterval) { this.maxInactiveInterval = maxInactiveInterval; }
	public void setMaxTotal(int maxTotal) { poolConfig.setMaxTotal(maxTotal); }
	public void setMaxIdle(int maxIdle) { poolConfig.setMaxIdle(maxIdle); }
	public void setMinIdle(int minIdle) { poolConfig.setMinIdle(minIdle); }
	public void setLifo(boolean lifo) { poolConfig.setLifo(lifo); }
	public void setFairness(boolean fairness) { poolConfig.setFairness(fairness); }
	public void setMaxWaitMillis(long maxWaitMillis) { poolConfig.setMaxWaitMillis(maxWaitMillis); }
	public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { poolConfig.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); }
	public void setSoftMinEvictableIdleTimeMillis(long softMinEvictableIdleTimeMillis) { poolConfig.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis); }
	public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) { poolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun); }
	public void setEvictionPolicyClassName(String evictionPolicyClassName) { poolConfig.setEvictionPolicyClassName(evictionPolicyClassName); }
	public void setTestOnCreate(boolean testOnCreate) { poolConfig.setTestOnCreate(testOnCreate); }
	public void setTestOnBorrow(boolean testOnBorrow) { poolConfig.setTestOnBorrow(testOnBorrow); }
	public void setTestOnReturn(boolean testOnReturn) { poolConfig.setTestOnReturn(testOnReturn); }
	public void setTestWhileIdle(boolean testWhileIdle) { poolConfig.setTestWhileIdle(testWhileIdle); }
	public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); }
	public void setBlockWhenExhausted(boolean blockWhenExhausted) { poolConfig.setBlockWhenExhausted(blockWhenExhausted); }
	public void setJmxEnabled(boolean jmxEnabled) { poolConfig.setJmxEnabled(jmxEnabled); }
	public void setJmxNamePrefix(String jmxNamePrefix) { poolConfig.setJmxNamePrefix(jmxNamePrefix); }
	public void setJmxNameBase(String jmxNameBase) { poolConfig.setJmxNameBase(jmxNameBase); }
	public void setHostAndPorts(String hostAndPorts) { this.hostAndPorts = hostAndPorts; }
}
